package mazewar.legacy;


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author jillsanluis
 */
public class NetworkServer implements Runnable {

    private static final int MAX_CLIENT_SIZE = 4;
    private Map<Integer, NetworkServerThread> clients = new ConcurrentHashMap<Integer, NetworkServerThread>();
    private ServerSocket server;
    private Thread thread;
    private int clientCount = 0;
    private long seed = 42;
    private static final int windowWidth = 20;
    private static final int windowHeight = 10;
    /**
     * The random number generator used by the {@link Maze}.
     */
    private Random randomGen;

    /**
     * Creates a new instance of MazeWarServer.
     * 
     * @param port  MazeWarServer listens to
     */
    public NetworkServer(int port) {

        try {
            System.out.println("Binding to port " + port + ", please wait...");
            server = new ServerSocket(port);
            System.out.println("Server started: " + server);

            randomGen = new Random(seed);

            start();
        } catch (IOException e) {
            System.err.println("Cannot bind to port " + port + ": " + e.getMessage());
        }
    }

    /**
     * Runs this instance of MazeWarServer.
     */
    public void run() {

        while (true) {
            try {
                System.out.println("Waiting for MazeWarClient ...");
                addThread(server.accept());
            } catch (IOException ex) {
                System.out.println("Server accept error: " + ex.getMessage());
                break;
            }
        }
    }

    /**
     * Start the MazeWarServer and creates its own thread.
     */
    private void start() {

        if (thread == null) {
            thread = new Thread(this);
            thread.start();
        }
    }

    /**
     * Adds MazeWar client to pool of clients handled by MazeWarServer.
     * 
     * @param socket    of the MazeWar client
     */
    private void addThread(Socket socket) {

        if (clientCount < MAX_CLIENT_SIZE) {
            System.out.println("Client accepted: " + socket);
            NetworkServerThread newClient = new NetworkServerThread(this, socket);
            clients.put(socket.getPort(), newClient);

            try {
                newClient.open();
                newClient.start();
                clientCount++;
            } catch (IOException e) {
                System.out.println("Error opening thread: " + e);
            }
        } else {
            System.out.println("MazeWarClient refused: maximum " + MAX_CLIENT_SIZE + " reached.");
        }
    }

    /**
     * Removes the MazeWar client from the server managed list of clients.
     *
     * @param id    to remove from client thread's list
     */
    public synchronized void remove(Integer id) {

        System.out.println("Removing client thread " + id);

        if (clients.containsKey(id)) {
            clients.remove(id);
            clientCount--;
        }
    }

    /**
     * Handles creation of broadcast packet to all clients.  It also updates the
     * client list if source client requested to quit from the game.
     *
     * @param id        of the source thread
     * @param packet    of sent by the source
     */
    public synchronized void handle(Integer id, NetworkPacket packet) {

        if (packet.getSrcInput() == null) {
            System.out.println("Ignoring invalid packet from client " + id);
            return;
        }

        System.out.println("Processing request from " + packet.getSrcName() + " " + packet.getSrcSocket() + " to " + packet.getSrcInput());

        // keep track of Threads which packet can't be sent to
        List<Integer> failWrite = new ArrayList<Integer>();

        NetworkServerThread requestingClient = null;
        if (packet.getSrcInput() == ClientSessionInput.JOIN) {
            requestingClient = clients.get(id);

            // find a starting point for this client
            // Pick a random starting point, and check to see if it is already occupied
            DirectedPoint dp = getUniquePoint(requestingClient.getClientKey());
            requestingClient.setDirectedPoint(dp);
            packet.setSrcDirectedPoint(dp);
            packet.setWindowHeight(windowHeight);
            packet.setWindowWidth(windowWidth);
        }

        else if(packet.getSrcInput() == ClientSessionInput.REBORN){

            NetworkServerThread rc = clients.get(id);
            DirectedPoint dp = getUniquePoint(rc.getClientKey());
rc.setDirectedPoint(dp);
          System.out.println(rc.getClientKey() + " is asking to be reborn");
          packet.setSrcDirectedPoint(dp);
          }
        // Now broadcast the updates to everyone
        for (NetworkServerThread client : clients.values()) {

            System.out.println("Sending the request to " + client.getClientKey());

            NetworkPacket toClient = packet.clone();
            toClient.setDstName(client.getRawName());
            toClient.setDstSocket(client.getID());
            
            try {
                client.send(toClient);
            } catch (IOException ex) {
                failWrite.add(client.getID());
            }

            if (requestingClient != null && !requestingClient.getClientKey().equals(client.getClientKey())) {
                System.out.println("Adding old player " + client.getClientKey());
                NetworkPacket addOldPlayers = new NetworkPacket();
                addOldPlayers.setSrcInput(packet.getSrcInput());
                addOldPlayers.setSrcName(toClient.getDstName());
                addOldPlayers.setSrcSocket(toClient.getDstSocket());
                addOldPlayers.setDstName(toClient.getSrcName());
                addOldPlayers.setDstSocket(toClient.getSrcSocket());
                addOldPlayers.setWindowHeight(windowHeight);
                addOldPlayers.setWindowWidth(windowWidth);
                addOldPlayers.setSrcDirectedPoint(client.getDirectedPoint());

                System.out.println("=======");
                System.out.println(addOldPlayers);
                
                try {
                    requestingClient.send(addOldPlayers);
                } catch (IOException ex) {
                    failWrite.add(client.getID());
                }

            }
        }

        // finally remove the client if it wants to quit
        if (packet.getSrcInput() == ClientSessionInput.QUIT) {
            remove(id);
        }

        if (!failWrite.isEmpty()) {
            System.out.println("Removing failed writing thread(s).");
        }

        for (Integer fw : failWrite) {
            System.out.println("socket writing failed: " + fw);
            remove(fw);
        }
    }

    /**
     * The main method to run during start up.
     *
     * @param args  command line parameters
     */
    public static void main(String args[]) {

        NetworkServer gameServer = null;
        if (args.length == 1) {
            try {
                gameServer = new NetworkServer(Integer.parseInt(args[0]));
            } catch (NumberFormatException e) {
                System.err.println("Port must be an integer [" + e.getMessage() + "]");
            }

        } else {
            System.err.println("Must specify listening port.");
        }
    }

  private synchronized DirectedPoint getUniquePoint(String key){
Point point = new Point(randomGen.nextInt(windowWidth), randomGen.nextInt(windowHeight));
            boolean unique = false;
            while (!unique) {
                unique = true;
                for (NetworkServerThread c : clients.values()) {
                    if (key != null && !key.equals(c.getClientKey())) {
                        if (point.getX() == c.getDirectedPoint().getX() && (point.getY()) == c.getDirectedPoint().getY()) {
                            unique = false;
                            break;
                        }
                    }
                }
            }

            return new DirectedPoint(point, Direction.random());
}
}
